package eval

import (
	"fmt"
	"github.com/hashicorp/hcl/v2"
	"github.com/ihaiker/tfwrapper/internal/config"
	"github.com/ihaiker/tfwrapper/internal/errors"
	"github.com/ihaiker/tfwrapper/internal/shells"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"
)

type evalFunction struct {
	Name       string            `hcl:"name,label" cty:"name"`
	Command    string            `hcl:"command,attr" cty:"command"`
	WorkingDir string            `hcl:"working_dir,attr" cty:"working_dir"`
	Args       []string          `hcl:"args,attr" cty:"args"`
	Env        map[string]string `hcl:"env,attr" cty:"env"`
}

func (ec *WrapperEvalContext) customDefinedFunction(fn config.Function) function.Function {
	return function.New(&function.Spec{
		VarParam: &function.Parameter{Type: cty.String},
		Type:     function.StaticReturnType(cty.String),
		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
			fun, diags := ec.convertFn(fn)
			if diags.HasErrors() {
				return cty.NilVal, diags.Errs()[0]
			}
			for _, arg := range args {
				if arg.IsNull() {
					return cty.NilVal, errors.New("function %s argument is null", fn.Name)
				}
				if arg.Type() != cty.String {
					return cty.NilVal, errors.New("function %s argument is null", fn.Name)
				}
				fun.Args = append(fun.Args, arg.AsString())
			}

			cmd := shells.New(fun.Command, fun.Args...).
				WorkingDir(fun.WorkingDir).
				Envs(fun.Env)

			stdout, stderr, err := cmd.RunWithOutput()
			if err != nil {
				err = errors.New(stderr)
			}
			return cty.StringVal(stdout), err
		},
	})
}

func (ec *WrapperEvalContext) convertFn(fn config.Function) (fun evalFunction, diags hcl.Diagnostics) {
	fun = evalFunction{
		Name: fn.Name,
	}
	if fun.WorkingDir, diags = ec.mustStringValue(fn.WorkingDir); diags.HasErrors() {
		return
	}
	if fun.Command, diags = ec.mustStringValue(fn.Command); diags.HasErrors() {
		return
	}
	if fun.Command == "" {
		diags = errors.SubjectDiagnostics(&fn.Command.Range,
			"Command is empty", fmt.Errorf("no `command` was found for method named %s", fn.Name))
		return
	}
	if fun.Args, diags = ec.mustStringSliceValues(fn.Args); diags.HasErrors() {
		return
	}
	return
}

func (ec *WrapperEvalContext) mustStringValue(attr *hcl.Attribute) (string, hcl.Diagnostics) {
	if attr != nil {
		if value, diags := attr.Expr.Value(ec.EvalContext); diags.HasErrors() {
			return "", diags
		} else if value.IsNull() {
			return "", diags
		} else if value.Type() != cty.String {

		} else {
			return value.AsString(), nil
		}
	}
	return "", nil
}

func (ec *WrapperEvalContext) mustStringSliceValues(attr *hcl.Attribute) (items []string, diags hcl.Diagnostics) {
	defer errors.Recover(func(cause error) {
		diags = errors.SubjectDiagnostics(&attr.Range, fmt.Sprintf("decode function args %s", attr.Name), cause)
	})
	value, diags := attr.Expr.Value(ec.EvalContext)
	if diags.HasErrors() {
		return nil, diags
	}
	values := make([]string, 0)
	for _, v := range value.AsValueSlice() {
		if v.IsNull() {
			continue
		}
		if v.Type() != cty.String {
			return nil, errors.SubjectDiagnostics(&attr.Range, "decode function args", fmt.Errorf("%s is not string", attr.Name))
		}
		values = append(values, v.AsString())
	}
	return values, nil
}
